/**
 * FiltersFeature is a grid {@link Ext.grid.feature.Feature feature} that allows
 * for a slightly more robust representation of filtering than what is provided
 * by the default store.
 * 
 * Filtering is adjusted by the user using the grid's column header menu (this
 * menu can be disabled through configuration). Through this menu users can
 * configure, enable, and disable filters for each column.
 * 
 * #Features#
 * 
 * ##Filtering implementations:##
 * 
 * Default filtering for Strings, Numeric Ranges, Date Ranges, Lists (which can
 * be backed by a {@link Ext.data.Store}), and Boolean. Additional custom
 * filter types and menus are easily created by extending
 * {@link Ext.ux.grid.filter.Filter}.
 * 
 * ##Graphical Indicators:##
 * 
 * Columns that are filtered have {@link #filterCls a configurable css class}
 * applied to the column headers.
 * 
 * ##Automatic Reconfiguration:##
 * 
 * Filters automatically reconfigure when the grid 'reconfigure' event fires.
 * 
 * ##Stateful:##
 * 
 * Filter information will be persisted across page loads by specifying a
 * `stateId` in the Grid configuration.
 * 
 * The filter collection binds to the
 * {@link Ext.grid.Panel#beforestaterestore beforestaterestore} and
 * {@link Ext.grid.Panel#beforestatesave beforestatesave} events in order to be
 * stateful.
 * 
 * ##GridPanel Changes:##
 *  - A `filters` property is added to the GridPanel using this feature. - A
 * `filterupdate` event is added to the GridPanel and is fired upon
 * onStateChange completion.
 * 
 * ##Server side code examples:##
 *  - [PHP](http://www.vinylfox.com/extjs/grid-filter-php-backend-code.php) -
 * (Thanks VinylFox) - [Ruby on
 * Rails](http://extjs.com/forum/showthread.php?p=77326#post77326) - (Thanks
 * Zyclops) - [Ruby on
 * Rails](http://extjs.com/forum/showthread.php?p=176596#post176596) - (Thanks
 * Rotomaul)
 * 
 * #Example usage:#
 * 
 * var store = Ext.create('Ext.data.Store', { pageSize: 15 ... });
 * 
 * var filtersCfg = { ftype: 'filters', autoReload: false, //don't reload
 * automatically local: true, //only filter locally // filters may be configured
 * through the plugin, // or in the column definition within the headers
 * configuration filters: [{ type: 'numeric', dataIndex: 'id' }, { type:
 * 'string', dataIndex: 'name' }, { type: 'numeric', dataIndex: 'price' }, {
 * type: 'date', dataIndex: 'dateAdded' }, { type: 'list', dataIndex: 'size',
 * options: ['extra small', 'small', 'medium', 'large', 'extra large'], phpMode:
 * true }, { type: 'boolean', dataIndex: 'visible' }] };
 * 
 * var grid = Ext.create('Ext.grid.Panel', { store: store, columns: ...,
 * features: [filtersCfg], height: 400, width: 700, bbar:
 * Ext.create('Ext.PagingToolbar', { store: store }) });
 *  // a filters property is added to the GridPanel grid.filters
 */
Ext.define('Ext.ux.grid.FiltersFeature', {
	extend : 'Ext.grid.feature.Feature',
	alias : 'feature.filters',
	uses : ['Ext.ux.grid.menu.ListMenu', 'Ext.ux.grid.menu.RangeMenu',
			'Ext.ux.grid.filter.BooleanFilter', 'Ext.ux.grid.filter.DateFilter',
			'Ext.ux.grid.filter.DateTimeFilter', 'Ext.ux.grid.filter.ListFilter',
			'Ext.ux.grid.filter.NumericFilter', 'Ext.ux.grid.filter.StringFilter'],

	/**
	 * @cfg {Boolean} autoReload Defaults to true, reloading the datasource when a
	 *      filter change happens. Set this to false to prevent the datastore from
	 *      being reloaded if there are changes to the filters. See `{@link #updateBuffer}`.
	 */
	autoReload : true,
	/**
	 * @cfg {Boolean} encode Specify true for {@link #buildQuery} to use
	 *      Ext.util.JSON.encode to encode the filter query parameter sent with a
	 *      remote request. Defaults to false.
	 */
	/**
	 * @cfg {Array} filters An Array of filters config objects. Refer to each
	 *      filter type class for configuration details specific to each filter
	 *      type. Filters for Strings, Numeric Ranges, Date Ranges, Lists, and
	 *      Boolean are the standard filters available.
	 */
	/**
	 * @cfg {String} filterCls The css class to be applied to column headers with
	 *      active filters. Defaults to `'ux-filterd-column'`
	 */
	filterCls : 'ux-filtered-column',
	/**
	 * @cfg {Boolean} local <tt>true</tt> to use Ext.data.Store filter functions
	 *      (local filtering) instead of the default (<tt>false</tt>) server
	 *      side filtering.
	 */
	local : false,
	/**
	 * @cfg {String} menuFilterText defaults to `'Filters'`.
	 */
	menuFilterText : 'Filters',
	/**
	 * @cfg {String} paramPrefix The url parameter prefix for the filters.
	 *      Defaults to `'filter'`.
	 */
	paramPrefix : 'filter',
	/**
	 * @cfg {Boolean} showMenu Defaults to true, including a filter submenu in the
	 *      default header menu.
	 */
	showMenu : true,
	/**
	 * @cfg {String} stateId Name of the value to be used to store state
	 *      information.
	 */
	stateId : undefined,
	/**
	 * @cfg {Number} updateBuffer Number of milliseconds to defer store updates
	 *      since the last filter change.
	 */
	updateBuffer : 500,

	// doesn't handle grid body events
	hasFeatureEvent : false,

	/** @private */
	constructor : function(config) {
		var me = this;

		me.callParent(arguments);

		me.deferredUpdate = Ext.create('Ext.util.DelayedTask', me.reload, me);

		// Init filters
		me.filters = me.createFiltersCollection();
		me.filterConfigs = config.filters;
	},

	init : function(grid) {
		var me = this, view = me.view, headerCt = view.headerCt;

		me.bindStore(view.getStore(), true);

		// Listen for header menu being created
		headerCt.on('menucreate', me.onMenuCreate, me);

		view.on('refresh', me.onRefresh, me);
		grid.on({
					scope : me,
					beforestaterestore : me.applyState,
					beforestatesave : me.saveState,
					beforedestroy : me.destroy
				});

		// Add event and filters shortcut on grid panel
		grid.filters = me;
		grid.addEvents('filterupdate');
		me.createFilters();
	},

	createFiltersCollection : function() {
		return Ext.create('Ext.util.MixedCollection', false, function(o) {
					return o ? o.dataIndex : null;
				});
	},

	/**
	 * @private Create the Filter objects for the current configuration,
	 *          destroying any existing ones first.
	 */
	createFilters : function() {
		var me = this, hadFilters = me.filters.getCount(), grid = me.getGridPanel(), filters = me
				.createFiltersCollection(), model = grid.store.model, fields = model.prototype.fields, field, filter, state;

		if (hadFilters) {
			state = {};
			me.saveState(null, state);
		}

		function add(dataIndex, config, filterable) {
			if (dataIndex && (filterable || config)) {
				field = fields.get(dataIndex);
				filter = {
					dataIndex : dataIndex,
					type : (field && field.type && field.type.type) || 'auto'
				};

				if (Ext.isObject(config)) {
					Ext.apply(filter, config);
				}

				filters.replace(filter);
			}
		}

		// We start with filters from our config
		Ext.Array.each(me.filterConfigs, function(filterConfig) {
					add(filterConfig.dataIndex, filterConfig);
				});

		// Then we merge on filters from the columns in the grid. The columns'
		// filters take precedence.
		Ext.Array.each(grid.columnManager.getColumns(), function(column) {
					if (column.filterable === false) {
						filters.removeAtKey(column.dataIndex);
					} else {
						add(column.dataIndex, column.filter, column.filterable);
					}
				});

		me.removeAll();
		if (filters.items) {
			me.initializeFilters(filters.items);
		}

		if (hadFilters) {
			me.applyState(null, state);
		}
	},

	/**
	 * @private
	 */
	initializeFilters : function(filters) {
		var me = this, filtersLength = filters.length, i, filter, FilterClass;

		for (i = 0; i < filtersLength; i++) {
			filter = filters[i];
			if (filter) {
				FilterClass = me.getFilterClass(filter.type);
				filter = filter.menu ? filter : new FilterClass(Ext.apply({
							grid : me.grid
						}, filter));
				me.filters.add(filter);
				Ext.util.Observable.capture(filter, me.onStateChange, me);
			}
		}
	},

	/**
	 * @private Handle creation of the grid's header menu. Initializes the filters
	 *          and listens for the menu being shown.
	 */
	onMenuCreate : function(headerCt, menu) {
		var me = this;

		// If the menu is ever destroyed, the filters need recreating because
		// the filters' menu structures will be destroyed.
		if (me.filtersNeedReCreating) {
			me.createFilters();
			me.filtersNeedReCreating = false;
		}
		menu.on({
					beforeshow : me.onMenuBeforeShow,
					destroy : me.onMenuDestroy,
					scope : me
				});
	},

	// The filters at first have to be created at init time so that state can be
	// restored if the grid subsequently
	// fires a beforestaterestore event.
	// However after that, they may need recreating if the column menu is ever
	// destroyed (due to column movement) because
	// that tears down the whole filter item and submenu structure.
	onMenuDestroy : function() {
		this.filtersNeedReCreating = true;
	},

	/**
	 * @private Handle showing of the grid's header menu. Sets up the filter item
	 *          and menu appropriate for the target column.
	 */
	onMenuBeforeShow : function(menu) {
		var me = this, menuItem, filter;

		if (me.showMenu) {
			menuItem = me.menuItem;
			if (!menuItem || menuItem.isDestroyed) {
				me.createMenuItem(menu);
				menuItem = me.menuItem;
			}

			filter = me.getMenuFilter();

			if (filter) {
				menuItem.setMenu(filter.menu, false);
				menuItem.setChecked(filter.active);
				// disable the menu if filter.disabled explicitly set to true
				menuItem.setDisabled(filter.disabled === true);
			}
			menuItem.setVisible(!!filter);
			me.sep.setVisible(!!filter);
		}
	},

	createMenuItem : function(menu) {
		var me = this;
		me.sep = menu.add('-');
		me.menuItem = menu.add({
					checked : false,
					itemId : 'filters',
					text : me.menuFilterText,
					listeners : {
						scope : me,
						checkchange : me.onCheckChange,
						beforecheckchange : me.onBeforeCheck
					}
				});
	},

	getGridPanel : function() {
		// This reference is injected in TableView.initFeatures
		return this.grid;
	},

	/**
	 * @private Handler for the grid's beforestaterestore event (fires before the
	 *          state of the grid is restored).
	 * @param {Object}
	 *          grid The grid object
	 * @param {Object}
	 *          state The hash of state values returned from the StateProvider.
	 */
	applyState : function(grid, state) {
		var me = this, key, filter;
		me.applyingState = true;
		me.clearFilters();
		if (state.filters) {
			for (key in state.filters) {
				if (state.filters.hasOwnProperty(key)) {
					filter = me.filters.get(key);
					if (filter) {
						filter.setValue(state.filters[key]);
						filter.setActive(true);
					}
				}
			}
		}
		me.deferredUpdate.cancel();
		if (me.local) {
			me.reload();
		}
		delete me.applyingState;
		delete state.filters;
	},

	/**
	 * Saves the state of all active filters
	 * 
	 * @param {Object}
	 *          grid
	 * @param {Object}
	 *          state
	 * @return {Boolean}
	 */
	saveState : function(grid, state) {
		var filters = {};
		this.filters.each(function(filter) {
					if (filter.active) {
						filters[filter.dataIndex] = filter.getValue();
					}
				});
		return (state.filters = filters);
	},

	/**
	 * @private Handler called by the grid 'beforedestroy' event
	 */
	destroy : function() {
		var me = this;
		Ext.destroyMembers(me, 'menuItem', 'sep');
		me.removeAll();
		me.clearListeners();
	},

	/**
	 * Remove all filters, permanently destroying them.
	 */
	removeAll : function() {
		if (this.filters) {
			Ext.destroy.apply(Ext, this.filters.items);
			// remove all items from the collection
			this.filters.clear();
		}
	},

	/**
	 * Changes the data store bound to this view and refreshes it.
	 * 
	 * @param {Ext.data.Store}
	 *          store The store to bind to this view
	 */
	bindStore : function(store) {
		var me = this;

		// Unbind from the old Store
		if (me.store && me.storeListeners) {
			me.store.un(me.storeListeners);
		}

		// Set up correct listeners
		if (store) {
			me.storeListeners = {
				scope : me
			};
			if (me.local) {
				me.storeListeners.load = me.onLoad;
			} else {
				me.storeListeners['before' + (store.buffered ? 'prefetch' : 'load')] = me.onBeforeLoad;
			}
			store.on(me.storeListeners);
		} else {
			delete me.storeListeners;
		}
		me.store = store;
	},

	/**
	 * @private Get the filter menu from the filters MixedCollection based on the
	 *          clicked header
	 */
	getMenuFilter : function() {
		var header = this.view.headerCt.getMenu().activeHeader;
		return header ? this.filters.get(header.dataIndex) : null;
	},

	/** @private */
	onCheckChange : function(item, value) {
		this.getMenuFilter().setActive(value);
	},

	/** @private */
	onBeforeCheck : function(check, value) {
		return !value || this.getMenuFilter().isActivatable();
	},

	/**
	 * @private Handler for all events on filters.
	 * @param {String}
	 *          event Event name
	 * @param {Object}
	 *          filter Standard signature of the event before the event is fired
	 */
	onStateChange : function(event, filter) {
		if (event !== 'serialize') {
			var me = this, grid = me.getGridPanel();

			if (filter == me.getMenuFilter()) {
				me.menuItem.setChecked(filter.active, false);
			}

			if ((me.autoReload || me.local) && !me.applyingState) {
				me.deferredUpdate.delay(me.updateBuffer);
			}
			me.updateColumnHeadings();

			if (!me.applyingState) {
				grid.saveState();
			}
			grid.fireEvent('filterupdate', me, filter);
		}
	},

	/**
	 * @private Handler for store's beforeload event when configured for remote
	 *          filtering
	 * @param {Object}
	 *          store
	 * @param {Object}
	 *          options
	 */
	onBeforeLoad : function(store, options) {
		var params;

		options.params = options.params || {};
		this.cleanParams(options.params);
		params = this.buildQuery(this.getFilterData());

		// Memory proxy
		if (store.getProxy().isSynchronous && this.hasActiveFilter()) {
			options.filters = [new Ext.util.Filter({
						filterFn : this.getRecordFilter()
					})];
		}

		Ext.apply(options.params, params);
	},

	/**
	 * @private Handler for store's load event when configured for local filtering
	 * @param {Object}
	 *          store
	 */
	onLoad : function(store) {
		if (this.filters.length) {
			store.filterBy(this.getRecordFilter());
		}
	},

	/**
	 * @private Handler called when the grid's view is refreshed
	 */
	onRefresh : function() {
		this.updateColumnHeadings();
	},

	/**
	 * Update the styles for the header row based on the active filters
	 */
	updateColumnHeadings : function() {
		var me = this, headerCt = me.view.headerCt;
		if (headerCt) {
			headerCt.items.each(function(header) {
				var filter = me.getFilter(header.dataIndex);
				header[filter && filter.active ? 'addCls' : 'removeCls'](me.filterCls);
				if (header.items && header.items['each'] ) {
					header.items.each(function(header) {
						var filter = me.getFilter(header.dataIndex);
						header[filter && filter.active ? 'addCls' : 'removeCls'](me.filterCls);
					})
				}
			});
		}
	},

	/** @private */
	reload : function() {
		var me = this, store = me.view.getStore();

		if (me.local) {
			store.clearFilter(true);
			store.filterBy(me.getRecordFilter());
			store.sort();
		} else {
			me.deferredUpdate.cancel();
			if (store.buffered) {
				store.data.clear();
			}
			store.loadPage(1);
		}
	},

	/**
	 * Method factory that generates a record validator for the filters active at
	 * the time of invokation.
	 * 
	 * @private
	 */
	getRecordFilter : function() {
		var f = [], len, i, lockingPartner = this.lockingPartner;

		this.filters.each(function(filter) {
					if (filter.active) {
						f.push(filter);
					}
				});

		// Be sure to check the active filters on a locking partner as well.
		if (lockingPartner) {
			lockingPartner.filters.each(function(filter) {
						if (filter.active) {
							f.push(filter);
						}
					});
		}

		len = f.length;
		return function(record) {
			for (i = 0; i < len; i++) {
				if (!f[i].validateRecord(record)) {
					return false;
				}
			}
			return true;
		};
	},

	hasActiveFilter : function() {
		var result = false;
		this.filters.each(function(filter) {
					if (filter.active) {
						result = true;
						return false;
					}
				});
		return result;
	},

	/**
	 * Adds a filter to the collection and observes it for state change.
	 * 
	 * @param {Object/Ext.ux.grid.filter.Filter}
	 *          config A filter configuration or a filter object.
	 * @return {Ext.ux.grid.filter.Filter} The existing or newly created filter
	 *         object.
	 */
	addFilter : function(config) {
		var me = this, columns = me.getGridPanel().columnManager.getColumns(), i, columnsLength, column, filtersLength, filter;

		for (i = 0, columnsLength = columns.length; i < columnsLength; i++) {
			column = columns[i];
			if (column.dataIndex === config.dataIndex) {
				column.filter = config;
			}
		}

		if (me.view.headerCt.menu) {
			me.createFilters();
		} else {
			// Call getMenu() to ensure the menu is created, and so, also are the
			// filters. We cannot call
			// createFilters() withouth having a menu because it will cause in a
			// recursion to applyState()
			// that ends up to clear all the filter values. This is likely to happen
			// when we reorder a column
			// and then add a new filter before the menu is recreated.
			me.view.headerCt.getMenu();
		}

		for (i = 0, filtersLength = me.filters.items.length; i < filtersLength; i++) {
			filter = me.filters.items[i];
			if (filter.dataIndex === config.dataIndex) {
				return filter;
			}
		}
	},

	/**
	 * Adds filters to the collection.
	 * 
	 * @param {Array}
	 *          filters An Array of filter configuration objects.
	 */
	addFilters : function(filters) {
		if (filters) {
			var me = this, i, filtersLength;
			for (i = 0, filtersLength = filters.length; i < filtersLength; i++) {
				me.addFilter(filters[i]);
			}
		}
	},

	/**
	 * Returns a filter for the given dataIndex, if one exists.
	 * 
	 * @param {String}
	 *          dataIndex The dataIndex of the desired filter object.
	 * @return {Ext.ux.grid.filter.Filter}
	 */
	getFilter : function(dataIndex) {
		return this.filters.get(dataIndex);
	},

	/**
	 * Turns all filters off. This does not clear the configuration information
	 * (see {@link #removeAll}).
	 */
	clearFilters : function() {
		this.filters.each(function(filter) {
					filter.setActive(false);
				});
	},

	getFilterItems : function() {
		var me = this;

		// If there's a locked grid then we must get the filter items for each grid.
		if (me.lockingPartner) {
			return me.filters.items.concat(me.lockingPartner.filters.items);
		}

		return me.filters.items;
	},

	/**
	 * Returns an Array of the currently active filters.
	 * 
	 * @return {Array} filters Array of the currently active filters.
	 */
	getFilterData : function() {
		var items = this.getFilterItems(), filters = [], n, nlen, item, d, i, len;

		for (n = 0, nlen = items.length; n < nlen; n++) {
			item = items[n];
			if (item.active) {
				d = [].concat(item.serialize());
				for (i = 0, len = d.length; i < len; i++) {
					filters.push({
								field : item.dataIndex,
								data : d[i]
							});
				}
			}
		}
		return filters;
	},

	/**
	 * Function to take the active filters data and build it into a query. The
	 * format of the query depends on the {@link #encode} configuration:
	 *  - `false` (Default) : Flatten into query string of the form (assuming
	 * <code>{@link #paramPrefix}='filters'</code>:
	 * 
	 * filters[0][field]="someDataIndex"&
	 * filters[0][data][comparison]="someValue1"&
	 * filters[0][data][type]="someValue2"& filters[0][data][value]="someValue3"&
	 * 
	 *  - `true` : JSON encode the filter data
	 * 
	 * {filters:[{"field":"someDataIndex","comparison":"someValue1","type":"someValue2","value":"someValue3"}]}
	 * 
	 * Override this method to customize the format of the filter query for remote
	 * requests.
	 * 
	 * @param {Array}
	 *          filters A collection of objects representing active filters and
	 *          their configuration. Each element will take the form of {field:
	 *          dataIndex, data: filterConf}. dataIndex is not assured to be
	 *          unique as any one filter may be a composite of more basic filters
	 *          for the same dataIndex.
	 * 
	 * @return {Object} Query keys and values
	 */
	buildQuery : function(filters) {
		var p = {}, i, f, root, dataPrefix, key, tmp, len = filters.length;

		if (!this.encode) {
			for (i = 0; i < len; i++) {
				f = filters[i];
				root = [this.paramPrefix, '[', i, ']'].join('');
				p[root + '[field]'] = f.field;

				dataPrefix = root + '[data]';
				for (key in f.data) {
					p[[dataPrefix, '[', key, ']'].join('')] = f.data[key];
				}
			}
		} else {
			tmp = [];
			for (i = 0; i < len; i++) {
				f = filters[i];
				tmp.push(Ext.apply({}, {
							field : f.field
						}, f.data));
			}
			// only build if there is active filter
			if (tmp.length > 0) {
				p[this.paramPrefix] = Ext.JSON.encode(tmp);
			}
		}
		return p;
	},

	/**
	 * Removes filter related query parameters from the provided object.
	 * 
	 * @param {Object}
	 *          p Query parameters that may contain filter related fields.
	 */
	cleanParams : function(p) {
		// if encoding just delete the property
		if (this.encode) {
			delete p[this.paramPrefix];
			// otherwise scrub the object of filter data
		} else {
			var regex, key;
			regex = new RegExp('^' + this.paramPrefix + '\[[0-9]+\]');
			for (key in p) {
				if (regex.test(key)) {
					delete p[key];
				}
			}
		}
	},

	/**
	 * Function for locating filter classes, overwrite this with your favorite
	 * loader to provide dynamic filter loading.
	 * 
	 * @param {String}
	 *          type The type of filter to load ('Filter' is automatically
	 *          appended to the passed type; eg, 'string' becomes 'StringFilter').
	 * @return {Function} The Ext.ux.grid.filter.Class
	 */
	getFilterClass : function(type) {
		// map the supported Ext.data.Field type values into a supported filter
		switch (type) {
			case 'auto' :
				type = 'string';
				break;
			case 'int' :
			case 'float' :
				type = 'numeric';
				break;
			case 'bool' :
				type = 'boolean';
				break;
		}
		return Ext.ClassManager.getByAlias('gridfilter.' + type);
	}
});
